package jalgebrava.group;

import jalgebrava.group.properties.GroupProperty;
import jalgebrava.util.F;
import jalgebrava.util.ProvidesObjectDelegates;
import jalgebrava.util.collections.ImmutableIterable;
import jalgebrava.util.collections.ImmutableIterator;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;


public abstract class Group<G> implements ElementsIterable<G>, ProvidesObjectDelegates<G>, HasSize {
	
	private static Logger logger = Logger.getLogger(Group.class);
	/**
	 * Contract: this.contains(this.unit())
	 */
	public abstract G unit();
	/**
	 * Contract: this.contains(x) => this.contains(this.inv(x))
	 */
	public abstract G inv(G x);
	/**
	 * Contract: this.contains(x) ^ this.contains(y) => this.contains(this.mul(x,y))
	 */
	public abstract G mul(G x, G y);
	public abstract boolean contains(G x);
	
		
	public final GroupElement<G> unitElement() {
		return GroupElement.mkAssert(this, unit());
	}
	public final GroupElement<G> invElement(GroupElement<G> x) {
		return GroupElement.mkAssert(this, inv(x.g));
	}
	public final GroupElement<G> mulElement(GroupElement<G> x, GroupElement<G> y) {
		return GroupElement.mkAssert(this, mul(x.g, y.g));
	}
	public final boolean containsElement(GroupElement<G> x) {
		return x.getGroup() == this;
	}
///////////////////////////////
	private final Map<String, Object> propNameToValue = new HashMap<String, Object>();
	public final <R> R get(GroupProperty<G, R> prop) {
		String propName = prop.getName();
		if (propNameToValue.containsKey(propName)) {			
			logger.debug("Using cached value of " + prop.toString() + " for " + this.getDescription()); 
			return (R) propNameToValue.get(propName);
		}
		logger.debug("Computing " + prop.toString() + " for " + this.getDescription());
		R result = prop.compute(this);
		propNameToValue.put(propName, result);
		return result;
	}
///////////////////////////////	
	private final String description;
	private final static Set<String> descriptions = new HashSet<String>();
	
	public final Group<G> getGroup() {
		return this;
	}
	protected Group(String inDescription) {
		this.description = inDescription;
		if (!descriptions.contains(description)) {
			descriptions.add(description);
		} else {
			logger.trace("Duplicate description: " + description);
		}
	}
	
	
	// could also be called inject, asElement
	public final GroupElement<G> element(G x) {
		return GroupElement.mk(this, x);
	}
	protected final GroupElement<G> elementAssert(G x) {
		return GroupElement.mkAssert(this, x);
	}
	public final F<G, GroupElement<G>> element = new F<G, GroupElement<G>>() {

		@Override
		public GroupElement<G> apply(G a) {
			return element(a);
		}
		
	};
	public abstract ImmutableIterator<G> iterator();
		
	public abstract ImmutableIterable<GroupElement<G>> elements();
	
	public final String getDescription() {
		return description;
	}
	
	public abstract String toShortString(G g);

	private final Map<G, Integer> orderMap = new HashMap<G,Integer>();
	public final int order(G x) {
		Integer result = orderMap.get(x);
		if (result != null) {
			return result;
		}
		// split this off
		LinkedList<G> powers = new LinkedList<G>();
		powers.add(unit());

		int order = 0;		
		G y = unit();
		
		do {
			y = mul(x, y);
			powers.add(y);
			order++;	
		} while (!equals(y,unit()));
		
		orderMap.put(x, order);
		for (int i=0;i<order;i++) {
			logger.trace("Order of " + toShortString(powers.get(i)) + " is set to " + order/ggd(i,order));
			orderMap.put(powers.get(i), order/ggd(i,order));
		}
		
		return order;
	}
	public static void main(String[] args) {
		System.out.println(ggd(0,7));
	}
	private static int ggd(int a, int b) {
		if (a*b == 0) {
			return a+b;
		}
		if (a == b) return a;
		if (a < b) return ggd(a, b%a);
		return ggd(b, a%b);
	}

}
